1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 module hip.audio.clip;
12 import hip.util.path : baseName;
13 import hip.error.handler;
14 import hip.audio_decoding.audio;
15 import hip.audio;
16 import hip.audio.audiosource;
17 import hip.api.data.asset;
18 public import hip.api.audio.audioclip;
19 
20 
21 union HipAudioBuffer
22 {
23     import hip.config.audio;
24     static if(HasOpenAL)
25     {
26         import bindbc.openal;
27         ALuint al;
28     }
29     static if(HasOpenSLES)
30     {
31         import opensles.sles;
32         import hip.audio.backend.sles;
33         SLIBuffer* sles;
34     }
35     static if(HasXAudio2)
36     {
37         import directx.xaudio2;
38         XAUDIO2_BUFFER* xaudio;
39     }
40     static if(HasWebAudio)
41     {
42         import hip.audio.backend.webaudio.clip;
43         size_t webaudio;
44     }
45     static if(HasAVAudioEngine)
46     {
47         import hip.audio.backend.avaudio.clip;
48         AVAudioPCMBuffer avaudio;
49     }
50 }
51 
52 struct HipAudioBufferWrapper
53 {
54     HipAudioBuffer buffer;
55     bool isAvailable;
56 }
57 
58 
59 
60 /**
61 * Wraps a decoder onto it. Basically an easier interface with some more controls
62 *  that would be needed inside specific APIs.
63 *
64 *   AudioClip flow basically consists in:
65 *   1. Initialize the audio clip with the current decoder.
66 *   2. Call `.load`, which calls `.decode`
67 *   3. HipAudioSource calls `.setClip`, which should call `clip.getBuffer`, which gets the buffer
68 *   wrapped by the current implementation `createBuffer`, and then the buffer is enqueued.
69 */
70 public abstract class HipAudioClip : HipAsset, IHipAudioClip
71 {
72     IHipAudioDecoder decoder;
73     ///Unused for non streamed. It is the binary loaded from a file which will be decoded
74     ubyte[] dataToDecode;
75     ///Unused for non streamed. Where the user will get its audio decoded.
76     ubyte[] outBuffer;
77     ///Unused for non streamed
78     uint chunkSize;
79 
80 
81     HipAudioClipHint hint;
82 
83     /**
84     *   Buffers recycled from HipAudioSource.
85     *
86     *   When source notifies that the buffer is free, it is added to
87     *   that array. When getBuffer is called, it could send one
88     *   of those recycleds.
89     */
90     private HipAudioBufferWrapper[] buffersToRecycle;
91     private HipAudioBufferWrapper[] buffersCreated;
92 
93     size_t totalDecoded = 0;
94 
95     HipAudioType type;
96     HipAudioEncoding encoding;
97     bool isStreamed = false;
98     string fileName;
99 
100 
101 
102     ///Event method called when the stream is updated
103     protected abstract void  onUpdateStream(ubyte[] data, uint decodedSize);
104     /**
105     *   Always alocates a pointer to the buffer data. So, after getting its content. Send it to the
106     *   recyclable buffers
107     */
108     protected abstract HipAudioBufferWrapper createBuffer(ubyte[] data);
109     protected abstract void  destroyBuffer(HipAudioBuffer* buffer);
110 
111     /** The buffer is actually any kind of external API buffer, it is the buffer contained in
112     *   HipAudioBufferWrapper.
113     *
114     *   OpenAL: `int` containing the buffer ID
115     *   OpenSL ES: `SLIBuffer`
116     *   XAudio2: To be thought?
117     */
118     public    abstract void  setBufferData(HipAudioBuffer* buffer, ubyte[] data, uint size);
119 
120     final immutable(HipAudioClipHint)* getHint(){return cast(immutable)&hint;}
121 
122     this()
123     {
124         super("HipAudioClip");
125         _typeID = assetTypeID!HipAudioClip();
126     }
127 
128     this(IHipAudioDecoder decoder, HipAudioClipHint hint){this(); this.decoder = decoder; this.hint = hint;}
129     this(IHipAudioDecoder decoder, HipAudioClipHint hint, uint chunkSize)
130     in(chunkSize > 0, "Chunk must be greater than 0")
131     {
132         this(decoder, hint);
133         this.chunkSize = chunkSize;
134         outBuffer = new ubyte[chunkSize];
135         ErrorHandler.assertExit(outBuffer != null, "Out of memory");
136     }
137     /**
138     *   Should implement the specific loading here
139     */
140     public bool loadFromMemory(in ubyte[] data, HipAudioEncoding encoding, HipAudioType type,
141     void delegate(in ubyte[]) onSuccess, void delegate() onFailure)
142     {
143         this.type = type;
144         this.isStreamed = false;
145         return decoder.loadData(data, encoding, type, hint, onSuccess, onFailure);
146     }
147     /**
148     *   Decodes a bit more of the current buffer
149     */
150     public final uint updateStream()
151     {
152         ErrorHandler.assertExit(chunkSize > 0, "Can't update stream with 0 sized buffer.");
153         uint dec = decoder.updateDecoding(outBuffer);
154         totalDecoded+= dec;
155         onUpdateStream(outBuffer, dec);
156         return dec;
157     }
158     package final HipAudioBufferWrapper* findBuffer(HipAudioBuffer buf)
159     {
160         foreach(ref b; buffersCreated)
161             if(b.buffer == buf)
162                 return &b;
163         return null;
164     }
165 
166     /**
167     *   Attempts to get a buffer from the buffer recycler.
168     *   Used for when loadStreamed must set a buffer available
169     */
170     public    final    HipAudioBuffer pollFreeBuffer()
171     {
172         if(buffersToRecycle.length > 0)
173         {
174             HipAudioBufferWrapper* w = &(buffersToRecycle[$ - 1]);
175             buffersToRecycle.length--;
176             w.isAvailable = false;
177             return w.buffer;
178         }
179         return HipAudioBuffer.init;
180     }
181 
182     public final HipAudioBuffer getBuffer(ubyte[] data, uint size)
183     {
184         HipAudioBuffer ret;
185         if((ret = pollFreeBuffer()) != HipAudioBuffer.init)
186         {
187             setBufferData(&ret, data, size);
188             return ret;
189         }
190         HipAudioBufferWrapper w = createBuffer(data);
191         setBufferData(&w.buffer, data, size);
192         ret = w.buffer;
193         buffersCreated~=w;
194         return ret;
195     }
196     HipAudioBufferAPI* _getBufferAPI(ubyte[] data, uint size)
197     {
198         HipAudioBuffer* temp = new HipAudioBuffer();
199         *temp = getBuffer(data, size);
200         return cast(HipAudioBufferAPI*)temp;
201     }
202     IHipAudioClip getAudioClipBackend(){return this;}
203 
204     package final void setBufferAvailable(HipAudioBuffer buffer)
205     {
206         HipAudioBufferWrapper* w = findBuffer(buffer);
207         ErrorHandler.assertExit(w != null, "AudioClip Error: No buffer was found when trying to set it available");
208         buffersToRecycle~= *w;
209         w.isAvailable = true;
210     }
211 
212     /**
213     *   Saves which data should be decoded and do 1 decoding frame
214     */
215     public uint loadStreamed(in ubyte[] data, HipAudioEncoding encoding)
216     {
217         dataToDecode = cast(ubyte[])data;
218         this.encoding = encoding;
219         ErrorHandler.assertExit(chunkSize > 0, "Can't update stream with 0 sized buffer.");
220         uint dec = decoder.startDecoding(dataToDecode, outBuffer, chunkSize, encoding);
221         totalDecoded+= dec;
222         onUpdateStream(outBuffer, dec);
223         return dec;
224     }
225 
226     ///Returns the streambuffer if streamed, else, returns entire sound
227     public ubyte[] getClipData()
228     {
229         if(isStreamed)
230             return outBuffer;
231         return decoder.getClipData();
232     }
233     ///Returns how much has been decoded
234     public size_t getClipSize()
235     {
236         if(isStreamed)
237             return totalDecoded;
238         return decoder.getClipSize();
239     }
240     public float getDuration(){return decoder.getDuration();}
241     public final uint getSampleRate(){return decoder.getSamplerate();}
242     public final float getDecodedDuration()
243     {
244         AudioConfig cfg = decoder.getAudioConfig();
245         return getClipSize() / (cast(float) cfg.sampleRate);
246     }
247 
248     override void onFinishLoading(){}
249     override bool isReady() const { return true; }
250 
251     override void onDispose()
252     {
253         decoder.dispose();
254         foreach (ref b; buffersCreated)
255             destroyBuffer(&b.buffer);
256         buffersCreated.length = 0;
257         if(outBuffer != null)
258         {
259             destroy(outBuffer);
260             outBuffer = null;
261         }
262     }
263 }
264 
265 
266 
267 /**
268  * Unpacks the HipAudioBufferAPI into a HipAudioBuffer.
269  *  ClipSize with size different than 0 is used for streamed audio
270  */
271 HipAudioBuffer getBufferFromAPI(IHipAudioClip clip, size_t clipSize = 0)
272 {
273     import hip.util.memory;
274     if(clipSize == 0)
275         clipSize = clip.getClipSize();
276     HipAudioBufferAPI* api = clip._getBufferAPI(clip.getClipData(), cast(uint)clipSize);
277     HipAudioBuffer buffer = *cast(HipAudioBuffer*)api;
278     freeGCMemory(api);
279     return buffer;
280 }